package org.teamswift.crow.rbac.security;

import com.auth0.jwt.exceptions.TokenExpiredException;
import com.google.common.base.Strings;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.Permission;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
import org.teamswift.crow.rbac.annotation.LoginRequiredOnly;
import org.teamswift.crow.rbac.annotation.PermissionAction;
import org.teamswift.crow.rbac.annotation.PublicAction;
import org.teamswift.crow.rbac.annotation.RequestedResource;
import org.teamswift.crow.rbac.entity.CrowUser;
import org.teamswift.crow.rbac.enums.CrowRbacErrors;
import org.teamswift.crow.rbac.enums.CrowResourceActions;
import org.teamswift.crow.rbac.exceptions.CredentialInvalidException;
import org.teamswift.crow.rbac.exceptions.CrowTokenInvalidException;
import org.teamswift.crow.rbac.security.permission.CrowPermission;
import org.teamswift.crow.rbac.security.token.CrowJWTToken;
import org.teamswift.crow.rbac.service.CrowUserService;
import org.teamswift.crow.rest.exception.BusinessException;
import org.teamswift.crow.rest.exception.impl.PermissionForbiddenException;
import org.teamswift.crow.rest.utils.CrowBeanUtils;
import org.teamswift.crow.rest.utils.CrowMessageUtil;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Set;

public class CrowSecurityInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {

        if(!(handler instanceof HandlerMethod)) {
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        Class<?> cls = handlerMethod.getBeanType();

        if(cls.isAssignableFrom(BasicErrorController.class)) {
            return true;
        }

        boolean isPublicAction = method.isAnnotationPresent(RequiresPermissions.class)
                || cls.isAnnotationPresent(PublicAction.class)
                || method.isAnnotationPresent(PublicAction.class);

        try {
            CrowSecurityUtils crowSecurityUtils = CrowBeanUtils.getBean(CrowSecurityUtils.class);
            String tokenStr = request.getHeader(crowSecurityUtils.getHeaderParam());
            if(Strings.isNullOrEmpty(tokenStr)) {
                tokenStr = request.getParameter("_token");
            }
            CrowJWTToken token = new CrowJWTToken(tokenStr);
            SecurityUtils.getSubject().login(token);
        } catch (TokenExpiredException e) {
            if(isPublicAction) {
                return true;
            }
            throw new CrowTokenInvalidException(
                    CrowMessageUtil.error(
                            CrowRbacErrors.TokenInvalid
                    )
            );
        } catch(Exception e) {
            if(isPublicAction) {
                return true;
            }
            throw new CredentialInvalidException(
                    CrowMessageUtil.error(
                            CrowRbacErrors.CredentialInvalid
                    )
            );
        }

        if(isPublicAction) {
            return true;
        }

        CrowUser user = (CrowUser) SecurityUtils.getSubject().getPrincipal();

        if(method.isAnnotationPresent(LoginRequiredOnly.class) && user != null) {
            return true;
        }

        // the requested resource
        String rawResource = null;
        RequestedResource requestedResource = cls.getAnnotation(RequestedResource.class);
        RequestMapping requestMapping = cls.getAnnotation(RequestMapping.class);
        if(requestedResource != null && !Strings.isNullOrEmpty(requestedResource.value())) {
            rawResource = requestedResource.value();
        } else if(requestMapping != null) {
            rawResource = requestMapping.value()[0];
        }

        if(Strings.isNullOrEmpty(rawResource)) {
            throw new BusinessException(
                    CrowMessageUtil.error(
                            CrowRbacErrors.RequestedResourceNotDefined
                    )
            );
        }

        String resource = rawResource.substring(1).replaceAll("/", ":");

        // action
        String action;
        PermissionAction permissionAction = method.getAnnotation(PermissionAction.class);
        if(permissionAction != null) {
            action = permissionAction.value();
        } else {
            RequestMapping methodRequestMapping = method.getAnnotation(RequestMapping.class);
            if(methodRequestMapping == null) {
                throw new BusinessException(
                        CrowMessageUtil.error(
                                CrowRbacErrors.RequestedResourceNotDefined
                        )
                );
            } else if(methodRequestMapping.method().length > 0) {
                action = methodRequestMapping.method()[0].name();
            } else {
                action = CrowResourceActions.GET.name();
            }
        }

        List<String> editActions = Arrays.asList("PUT", "DELETE", "POST");
        if(editActions.contains(action)) {
            action = "MANAGE";
        }

        CrowPermission needPerm = new CrowPermission();
        needPerm.setResource(resource);
        needPerm.setAction(action);

        CrowUserService crowUserService = CrowBeanUtils.getBean(CrowUserService.class);

        if(user == null) {
            throw new CredentialInvalidException(
                    CrowMessageUtil.error(
                            CrowRbacErrors.CredentialInvalid
                    )
            );
        }

        if(user.isSu()) {
            return true;
        }

        Set<Permission> permissions = crowUserService.getPermissions(user);

        for(Permission perm: permissions) {
            if(perm.implies(needPerm)) {
                return true;
            }
        }

        throw new PermissionForbiddenException(
                CrowMessageUtil.error(CrowRbacErrors.PermissionDenied)
        );

    }
}
